# coding: utf-8
import base64
from enum import Enum
from json import loads
from pathlib import Path
from typing import Optional, Union, Callable, Literal
from urllib import parse
from uuid import uuid4

from qtpy.QtCore import QUrl, QObject, Signal, QByteArray, QJsonDocument, QFile, QIODevice
from qtpy.QtGui import QPixmap, QImage
from qtpy.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkCookieJar, QHttpMultiPart, \
    QHttpPart, QNetworkProxy, QNetworkReply
from .setting import USER_AGENT


class ContentType(Enum):
    """
    常见的HTTP Content-Type。
    """
    JSON = "application/json"
    FORM = "application/x-www-form-urlencoded"
    MULTIPART = "multipart/form-data; boundary=---{}"
    TEXT = "text/plain"
    HTML = "text/html"
    XML = "application/xml"
    BINARY = "application/octet-stream"

    @property
    def to_bytes(self, charset: str = "utf-8") -> bytes:
        return self.value.encode(charset)

    @property
    def value(self):
        if self == ContentType.MULTIPART:
            return super().value.format(uuid4().hex)
        return super().value


class RequestKwargs(Enum):
    pass


class NetworkRequest(QNetworkRequest):
    """
    对QNetworkRequest进行封装，提供更方便的接口。
    """

    def __init__(
            self,
            url: Union[str, QUrl],
            params: Optional[dict] = None,
            headers: Optional[dict] = None,
            cookies: Optional[Union[dict, QNetworkCookieJar]] = None,
            auth: Optional[Union[str, tuple]] = None,
            timeout: Optional[int] = None,
            allow_redirects: bool = True,
    ):
        """
        :param url: URL for the request.
        :param params: Dictionary of query parameters to send with the :class:`Request`.
        :param headers: Dictionary of HTTP Headers to send with the :class:`Request`.
        :param cookies: Dict or CookieJar object to send with the :class:`Request`.
        :param auth: Tuple of username and password for Basic/Digest/Custom HTTP Auth.
        :param timeout: Timeout in seconds to wait for the server to send data before giving up.
        :param allow_redirects: Whether or not to follow redirects.
        """
        super().__init__(QUrl(url))
        self.set_params(params)
        self.set_headers(headers)
        self.set_cookies(cookies)
        self.set_auth(auth)
        self.set_timeout(timeout)
        self.set_allow_redirects(allow_redirects)

    def set_params(self, params: dict):
        """
        Set query parameters for the request.
        :param params: Dictionary of query parameters to send with the :class:`Request`.
        """
        if not params:
            return
        params = parse.urlencode(params)
        self.setUrl(QUrl(f"{self.url().toString()}?{params}"))

    def set_headers(self, headers: dict):
        """
        Set headers for the request.
        :param headers: Dictionary of HTTP Headers to send with the :class:`Request`.
        """
        if not headers:
            self.setRawHeader(b"User-Agent", USER_AGENT.encode())
            return
        for key, value in headers.items():
            self.setRawHeader(key.encode(), value.encode())

    def set_cookies(self, cookies: Union[dict, QNetworkCookieJar]):
        """
        Set cookies for the request.
        :param cookies: Dict or CookieJar object to send with the :class:`Request`.
        """
        if not cookies:
            return
        if isinstance(cookies, QNetworkCookieJar):
            for cookie in cookies.allCookies():
                self.setRawHeader(b"Cookie", cookie.toRawForm())
        else:
            for key, value in cookies.items():
                self.setRawHeader(key.encode(), value.encode())

    def set_auth(self, auth: Union[str, tuple]):
        """
        Set authentication for the request.
        :param auth: Tuple of username and password for Basic/Digest/Custom HTTP Auth.
        """
        if not auth:
            return
        if isinstance(auth, tuple):
            authorization = b"Basic " + base64.b64encode(f"{auth[0]}:{auth[1]}".encode())
        else:
            authorization = auth.encode()
        self.setRawHeader(b"Authorization", authorization)

    def set_timeout(self, timeout: int):
        """
        Set timeout for the request.
        :param timeout: Timeout in seconds.
        """
        if not timeout:
            return
        self.setTransferTimeout(timeout * 1000)

    def set_allow_redirects(self, allow_redirects: bool):
        """
        Set allow_redirects for the request.
        :param allow_redirects: Whether or not to follow redirects.
        """
        try:
            self.setAttribute(QNetworkRequest.FollowRedirectsAttribute, allow_redirects)
        except AttributeError:
            self.setAttribute(QNetworkRequest.Attribute.RedirectionTargetAttribute, allow_redirects)


class NetworkResponse:
    """
    对网络请求的响应进行封装。
    """

    def __init__(self, reply: QNetworkReply):
        self.reply = reply

    @property
    def errorString(self) -> str:
        return self.reply.errorString()

    @property
    def error(self) -> QNetworkReply.NetworkError | int:
        return self.reply.error()

    def isOk(self) -> bool:
        return self.error == QNetworkReply.NoError

    def isError(self) -> bool:
        return not self.isOk()

    def isRedirect(self) -> bool:
        return self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) in (301, 302, 303, 307)

    def isClientError(self) -> bool:
        return 400 <= self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) < 500

    def isServerError(self) -> bool:
        return 500 <= self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) < 600

    def isNetworkError(self) -> bool:
        return self.reply.error() != QNetworkReply.NoError and not self.isRedirect()

    @property
    def url(self) -> str:
        return self.reply.url().toString()

    @property
    def status_code(self) -> int:
        return self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

    @property
    def data(self) -> bytes:
        return self.reply.readAll().data()

    @property
    def text(self) -> str:
        return self.reply.readAll().data().decode()

    def json(self) -> dict:
        return loads(self.text)

    def headers(self) -> dict:
        headers = {}
        key: QByteArray
        value: QByteArray
        for key, value in self.reply.rawHeaderPairs():
            headers[key.data().decode()] = value.data().decode()
        return headers

    @property
    def content(self) -> bytes:
        return self.reply.readAll().data()

    def image(self, error_image: str = None) -> QImage:
        image = QImage()
        if self.isOk():
            image.loadFromData(self.content)
        else:
            if error_image:
                image.load(error_image)
        return image

    def pixmap(self, error_image: str = None) -> QPixmap:
        return QPixmap.fromImage(self.image(error_image))

    def saveImage(self, path: Union[str, Path]):
        if isinstance(path, Path):
            path = str(path)
        self.image().save(path)

    def deleteLater(self):
        self.reply.deleteLater()

    def __del__(self):
        self.deleteLater()


class HTTPRequest(QObject):
    """
    一个用于发出HTTP请求的类。
    该类不支持上传文件，如果需要上传文件，请使用requests库。
    """
    finished = Signal(NetworkResponse)
    errorOccurred = Signal(NetworkResponse)

    def __init__(
            self,
            method: Literal["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "TRACE"],
            url: Union[str, QUrl],
            params: Optional[dict] = None,
            data: Optional[dict] = None,
            json: Optional[dict] = None,
            headers: Optional[dict] = None,
            cookies: Optional[Union[dict, QNetworkCookieJar]] = None,
            auth: Optional[Union[str, tuple]] = None,
            timeout: Optional[int] = None,
            allow_redirects: bool = True,
            proxy: Optional[QNetworkProxy] = None,
            response_callback: Callable = None,
            parent: Optional[QObject] = None
    ):
        super().__init__(parent)
        self.manager = QNetworkAccessManager(self)
        if proxy:
            self.manager.setProxy(proxy)
        request = NetworkRequest(
            url,
            params=params,
            headers=headers,
            cookies=cookies,
            auth=auth,
            timeout=timeout,
            allow_redirects=allow_redirects,
        )
        if isinstance(method, str):
            method = method.encode()
        # 该区域存疑，可能存在问题，待修复，请尽量使用requests
        if data and not json:
            request_data = QJsonDocument(data).toJson()
            request.setRawHeader(b"Content-Type", ContentType.FORM.to_bytes)
        elif not data and json:
            request_data = QJsonDocument(json).toJson()
            request.setRawHeader(b"Content-Type", ContentType.JSON.to_bytes)
        elif data and json:
            json.update(data)
            request_data = QJsonDocument(json).toJson()
            request.setRawHeader(b"Content-Type", ContentType.JSON.to_bytes)
        else:
            request_data = None
        request.set_headers(headers)
        self.reply = self.manager.sendCustomRequest(request, method.upper(), request_data)
        self.reply.errorOccurred.connect(lambda reply=self.reply: self.__onError(reply))
        self.reply.finished.connect(lambda reply=self.reply: self.__on_finished(reply))
        if response_callback:
            self.finished.connect(response_callback)

    def __on_finished(self, reply: QNetworkReply):
        response = NetworkResponse(reply)
        self.finished.emit(response)

    def __onError(self, reply: QNetworkReply):
        response = NetworkResponse(reply)
        self.errorOccurred.emit(response)

    def __setMultiPart(self, files: dict):
        multiPart = QHttpMultiPart(QHttpMultiPart.ContentType.FormDataType)
        for name, file in files.items():
            httpPart = QHttpPart()
            if isinstance(file, tuple):
                if len(file) == 2:
                    fileName, filePath = file
                    content_type = None
                    custom_headers = {}
                elif len(file) == 3:
                    fileName, filePath, content_type = file
                    custom_headers = {}
                elif len(file) == 4:
                    fileName, filePath, content_type, custom_headers = file
                else:
                    raise ValueError("Invalid file tuple")
                httpPart.setHeader(QNetworkRequest.ContentDispositionHeader,
                                   f"form-data; name=\"{name}\"; filename=\"{fileName}\"".encode())
                if content_type:
                    httpPart.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
                for key, value in custom_headers.items():
                    httpPart.setRawHeader(key.encode(), value.encode())
                fileobj = QFile(filePath)
                fileobj.open(QIODevice.ReadOnly)
                httpPart.setBodyDevice(fileobj)
            else:
                httpPart.setHeader(QNetworkRequest.ContentDispositionHeader, f"form-data; name=\"{name}\"")
                httpPart.setBodyDevice(None)
            multiPart.append(httpPart)
        return multiPart

    @staticmethod
    def get(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("GET", url, **kwargs)

    @staticmethod
    def post(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("POST", url, **kwargs)

    @staticmethod
    def put(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("PUT", url, **kwargs)

    @staticmethod
    def delete(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("DELETE", url, **kwargs)

    @staticmethod
    def head(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("HEAD", url, **kwargs)

    @staticmethod
    def options(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("OPTIONS", url, **kwargs)

    @staticmethod
    def patch(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("PATCH", url, **kwargs)

    @staticmethod
    def trace(url: Union[str, QUrl], **kwargs):
        return HTTPRequest("TRACE", url, **kwargs)


if __name__ == '__main__':
    from qtpy.QtWidgets import QApplication

    app = QApplication([])
    request = HTTPRequest.post(
        "https://httpbin.org/post",
        headers={"Content-Type": ContentType.JSON.value},
        data={"key": "value"},
        json={"key1": "value1"},
        params={"key": "value"},
        response_callback=lambda response: print(response.text)
    )

    app.exec_()
